<!DOCTYPE html>
<!--
@license
Copyright (C) 2017 The Android Open Source Project

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-router</title>

<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>

<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>

<test-fixture id="basic">
  <template>
    <gr-router></gr-router>
  </template>
</test-fixture>

<script type="module">
import '../../../test/common-test-setup.js';
import './gr-router.js';
suite('gr-router tests', () => {
  let element;
  let sandbox;

  setup(() => {
    sandbox = sinon.sandbox.create();
    element = fixture('basic');
  });

  teardown(() => { sandbox.restore(); });

  test('_firstCodeBrowserWeblink', () => {
    assert.deepEqual(element._firstCodeBrowserWeblink([
      {name: 'gitweb'},
      {name: 'gitiles'},
      {name: 'browse'},
      {name: 'test'}]), {name: 'gitiles'});

    assert.deepEqual(element._firstCodeBrowserWeblink([
      {name: 'gitweb'},
      {name: 'test'}]), {name: 'gitweb'});
  });

  test('_getBrowseCommitWeblink', () => {
    const browserLink = {name: 'browser', url: 'browser/url'};
    const link = {name: 'test', url: 'test/url'};
    const weblinks = [browserLink, link];
    const config = {gerrit: {primary_weblink_name: browserLink.name}};
    sandbox.stub(element, '_firstCodeBrowserWeblink').returns(link);

    assert.deepEqual(element._getBrowseCommitWeblink(weblinks, config),
        browserLink);

    assert.deepEqual(element._getBrowseCommitWeblink(weblinks, {}), link);
  });

  test('_getChangeWeblinks', () => {
    const link = {name: 'test', url: 'test/url'};
    const browserLink = {name: 'browser', url: 'browser/url'};
    const mapLinksToConfig = weblinks => { return {options: {weblinks}}; };
    sandbox.stub(element, '_getBrowseCommitWeblink').returns(browserLink);

    assert.deepEqual(
        element._getChangeWeblinks(mapLinksToConfig([link, browserLink]))[0],
        {name: 'test', url: 'test/url'});

    assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0],
        {name: 'test', url: 'test/url'});

    link.url = 'https://' + link.url;
    assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0],
        {name: 'test', url: 'https://test/url'});
  });

  test('_getHashFromCanonicalPath', () => {
    let url = '/foo/bar';
    let hash = element._getHashFromCanonicalPath(url);
    assert.equal(hash, '');

    url = '';
    hash = element._getHashFromCanonicalPath(url);
    assert.equal(hash, '');

    url = '/foo#bar';
    hash = element._getHashFromCanonicalPath(url);
    assert.equal(hash, 'bar');

    url = '/foo#bar#baz';
    hash = element._getHashFromCanonicalPath(url);
    assert.equal(hash, 'bar#baz');

    url = '#foo#bar#baz';
    hash = element._getHashFromCanonicalPath(url);
    assert.equal(hash, 'foo#bar#baz');
  });

  suite('_parseLineAddress', () => {
    test('returns null for empty and invalid hashes', () => {
      let actual = element._parseLineAddress('');
      assert.isNull(actual);

      actual = element._parseLineAddress('foobar');
      assert.isNull(actual);

      actual = element._parseLineAddress('foo123');
      assert.isNull(actual);

      actual = element._parseLineAddress('123bar');
      assert.isNull(actual);
    });

    test('parses correctly', () => {
      let actual = element._parseLineAddress('1234');
      assert.isOk(actual);
      assert.equal(actual.lineNum, 1234);
      assert.isFalse(actual.leftSide);

      actual = element._parseLineAddress('a4');
      assert.isOk(actual);
      assert.equal(actual.lineNum, 4);
      assert.isTrue(actual.leftSide);

      actual = element._parseLineAddress('b77');
      assert.isOk(actual);
      assert.equal(actual.lineNum, 77);
      assert.isTrue(actual.leftSide);
    });
  });

  test('_startRouter requires auth for the right handlers', () => {
    // This test encodes the lists of route handler methods that gr-router
    // automatically checks for authentication before triggering.

    const requiresAuth = {};
    const doesNotRequireAuth = {};
    sandbox.stub(Gerrit.Nav, 'setup');
    sandbox.stub(window.page, 'start');
    sandbox.stub(window.page, 'base');
    sandbox.stub(window, 'page');
    sandbox.stub(element, '_mapRoute', (pattern, methodName, usesAuth) => {
      if (usesAuth) {
        requiresAuth[methodName] = true;
      } else {
        doesNotRequireAuth[methodName] = true;
      }
    });
    element._startRouter();

    const actualRequiresAuth = Object.keys(requiresAuth);
    actualRequiresAuth.sort();
    const actualDoesNotRequireAuth = Object.keys(doesNotRequireAuth);
    actualDoesNotRequireAuth.sort();

    const shouldRequireAutoAuth = [
      '_handleAgreementsRoute',
      '_handleChangeEditRoute',
      '_handleCreateGroupRoute',
      '_handleCreateProjectRoute',
      '_handleDiffEditRoute',
      '_handleGroupAuditLogRoute',
      '_handleGroupInfoRoute',
      '_handleGroupListFilterOffsetRoute',
      '_handleGroupListFilterRoute',
      '_handleGroupListOffsetRoute',
      '_handleGroupMembersRoute',
      '_handleGroupRoute',
      '_handleGroupSelfRedirectRoute',
      '_handleNewAgreementsRoute',
      '_handlePluginListFilterOffsetRoute',
      '_handlePluginListFilterRoute',
      '_handlePluginListOffsetRoute',
      '_handlePluginListRoute',
      '_handleRepoCommandsRoute',
      '_handleSettingsLegacyRoute',
      '_handleSettingsRoute',
    ];
    assert.deepEqual(actualRequiresAuth, shouldRequireAutoAuth);

    const unauthenticatedHandlers = [
      '_handleBranchListFilterOffsetRoute',
      '_handleBranchListFilterRoute',
      '_handleBranchListOffsetRoute',
      '_handleChangeNumberLegacyRoute',
      '_handleChangeRoute',
      '_handleDiffRoute',
      '_handleDefaultRoute',
      '_handleChangeLegacyRoute',
      '_handleDiffLegacyRoute',
      '_handleDocumentationRedirectRoute',
      '_handleDocumentationSearchRoute',
      '_handleDocumentationSearchRedirectRoute',
      '_handleLegacyLinenum',
      '_handleImproperlyEncodedPlusRoute',
      '_handlePassThroughRoute',
      '_handleProjectDashboardRoute',
      '_handleProjectsOldRoute',
      '_handleRepoAccessRoute',
      '_handleRepoDashboardsRoute',
      '_handleRepoListFilterOffsetRoute',
      '_handleRepoListFilterRoute',
      '_handleRepoListOffsetRoute',
      '_handleRepoRoute',
      '_handleQueryLegacySuffixRoute',
      '_handleQueryRoute',
      '_handleRegisterRoute',
      '_handleTagListFilterOffsetRoute',
      '_handleTagListFilterRoute',
      '_handleTagListOffsetRoute',
      '_handlePluginScreen',
    ];

    // Handler names that check authentication themselves, and thus don't need
    // it performed for them.
    const selfAuthenticatingHandlers = [
      '_handleDashboardRoute',
      '_handleCustomDashboardRoute',
      '_handleRootRoute',
    ];

    const shouldNotRequireAuth = unauthenticatedHandlers
        .concat(selfAuthenticatingHandlers);
    shouldNotRequireAuth.sort();
    assert.deepEqual(actualDoesNotRequireAuth, shouldNotRequireAuth);
  });

  test('_redirectIfNotLoggedIn while logged in', () => {
    sandbox.stub(element.$.restAPI, 'getLoggedIn')
        .returns(Promise.resolve(true));
    const data = {canonicalPath: ''};
    const redirectStub = sandbox.stub(element, '_redirectToLogin');
    return element._redirectIfNotLoggedIn(data).then(() => {
      assert.isFalse(redirectStub.called);
    });
  });

  test('_redirectIfNotLoggedIn while logged out', () => {
    sandbox.stub(element.$.restAPI, 'getLoggedIn')
        .returns(Promise.resolve(false));
    const redirectStub = sandbox.stub(element, '_redirectToLogin');
    const data = {canonicalPath: ''};
    return new Promise(resolve => {
      element._redirectIfNotLoggedIn(data)
          .then(() => {
            assert.isTrue(false, 'Should never execute');
          })
          .catch(() => {
            assert.isTrue(redirectStub.calledOnce);
            resolve();
          });
    });
  });

  suite('generateUrl', () => {
    test('search', () => {
      let params = {
        view: Gerrit.Nav.View.SEARCH,
        owner: 'a%b',
        project: 'c%d',
        branch: 'e%f',
        topic: 'g%h',
        statuses: ['op%en'],
      };
      assert.equal(element._generateUrl(params),
          '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
          'topic:"g%2525h"+status:op%2525en');

      params.offset = 100;
      assert.equal(element._generateUrl(params),
          '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
          'topic:"g%2525h"+status:op%2525en,100');
      delete params.offset;

      // The presence of the query param overrides other params.
      params.query = 'foo$bar';
      assert.equal(element._generateUrl(params), '/q/foo%2524bar');

      params.offset = 100;
      assert.equal(element._generateUrl(params), '/q/foo%2524bar,100');

      params = {
        view: Gerrit.Nav.View.SEARCH,
        statuses: ['a', 'b', 'c'],
      };
      assert.equal(element._generateUrl(params),
          '/q/(status:a OR status:b OR status:c)');
    });

    test('change', () => {
      const params = {
        view: Gerrit.Nav.View.CHANGE,
        changeNum: '1234',
        project: 'test',
      };
      const paramsWithQuery = {
        view: Gerrit.Nav.View.CHANGE,
        changeNum: '1234',
        project: 'test',
        querystring: 'revert&foo=bar',
      };

      assert.equal(element._generateUrl(params), '/c/test/+/1234');
      assert.equal(element._generateUrl(paramsWithQuery),
          '/c/test/+/1234?revert&foo=bar');

      params.patchNum = 10;
      assert.equal(element._generateUrl(params), '/c/test/+/1234/10');
      paramsWithQuery.patchNum = 10;
      assert.equal(element._generateUrl(paramsWithQuery),
          '/c/test/+/1234/10?revert&foo=bar');

      params.basePatchNum = 5;
      assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10');
      paramsWithQuery.basePatchNum = 5;
      assert.equal(element._generateUrl(paramsWithQuery),
          '/c/test/+/1234/5..10?revert&foo=bar');

      params.messageHash = '#123';
      assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10#123');
    });

    test('change with repo name encoding', () => {
      const params = {
        view: Gerrit.Nav.View.CHANGE,
        changeNum: '1234',
        project: 'x+/y+/z+/w',
      };
      assert.equal(element._generateUrl(params),
          '/c/x%252B/y%252B/z%252B/w/+/1234');
    });

    test('diff', () => {
      const params = {
        view: Gerrit.Nav.View.DIFF,
        changeNum: '42',
        path: 'x+y/path.cpp',
        patchNum: 12,
      };
      assert.equal(element._generateUrl(params),
          '/c/42/12/x%252By/path.cpp');

      params.project = 'test';
      assert.equal(element._generateUrl(params),
          '/c/test/+/42/12/x%252By/path.cpp');

      params.basePatchNum = 6;
      assert.equal(element._generateUrl(params),
          '/c/test/+/42/6..12/x%252By/path.cpp');

      params.path = 'foo bar/my+file.txt%';
      params.patchNum = 2;
      delete params.basePatchNum;
      assert.equal(element._generateUrl(params),
          '/c/test/+/42/2/foo+bar/my%252Bfile.txt%2525');

      params.path = 'file.cpp';
      params.lineNum = 123;
      assert.equal(element._generateUrl(params),
          '/c/test/+/42/2/file.cpp#123');

      params.leftSide = true;
      assert.equal(element._generateUrl(params),
          '/c/test/+/42/2/file.cpp#b123');
    });

    test('diff with repo name encoding', () => {
      const params = {
        view: Gerrit.Nav.View.DIFF,
        changeNum: '42',
        path: 'x+y/path.cpp',
        patchNum: 12,
        project: 'x+/y',
      };
      assert.equal(element._generateUrl(params),
          '/c/x%252B/y/+/42/12/x%252By/path.cpp');
    });

    test('edit', () => {
      const params = {
        view: Gerrit.Nav.View.EDIT,
        changeNum: '42',
        project: 'test',
        path: 'x+y/path.cpp',
      };
      assert.equal(element._generateUrl(params),
          '/c/test/+/42/x%252By/path.cpp,edit');
    });

    test('_getPatchRangeExpression', () => {
      const params = {};
      let actual = element._getPatchRangeExpression(params);
      assert.equal(actual, '');

      params.patchNum = 4;
      actual = element._getPatchRangeExpression(params);
      assert.equal(actual, '4');

      params.basePatchNum = 2;
      actual = element._getPatchRangeExpression(params);
      assert.equal(actual, '2..4');

      delete params.patchNum;
      actual = element._getPatchRangeExpression(params);
      assert.equal(actual, '2..');
    });

    suite('dashboard', () => {
      test('self dashboard', () => {
        const params = {
          view: Gerrit.Nav.View.DASHBOARD,
        };
        assert.equal(element._generateUrl(params), '/dashboard/self');
      });

      test('user dashboard', () => {
        const params = {
          view: Gerrit.Nav.View.DASHBOARD,
          user: 'user',
        };
        assert.equal(element._generateUrl(params), '/dashboard/user');
      });

      test('custom self dashboard, no title', () => {
        const params = {
          view: Gerrit.Nav.View.DASHBOARD,
          sections: [
            {name: 'section 1', query: 'query 1'},
            {name: 'section 2', query: 'query 2'},
          ],
        };
        assert.equal(
            element._generateUrl(params),
            '/dashboard/?section%201=query%201&section%202=query%202');
      });

      test('custom repo dashboard', () => {
        const params = {
          view: Gerrit.Nav.View.DASHBOARD,
          sections: [
            {name: 'section 1', query: 'query 1 ${project}'},
            {name: 'section 2', query: 'query 2 ${repo}'},
          ],
          repo: 'repo-name',
        };
        assert.equal(
            element._generateUrl(params),
            '/dashboard/?section%201=query%201%20repo-name&' +
            'section%202=query%202%20repo-name');
      });

      test('custom user dashboard, with title', () => {
        const params = {
          view: Gerrit.Nav.View.DASHBOARD,
          user: 'user',
          sections: [{name: 'name', query: 'query'}],
          title: 'custom dashboard',
        };
        assert.equal(
            element._generateUrl(params),
            '/dashboard/user?name=query&title=custom%20dashboard');
      });

      test('repo dashboard', () => {
        const params = {
          view: Gerrit.Nav.View.DASHBOARD,
          repo: 'gerrit/repo',
          dashboard: 'default:main',
        };
        assert.equal(
            element._generateUrl(params),
            '/p/gerrit/repo/+/dashboard/default:main');
      });

      test('project dashboard (legacy)', () => {
        const params = {
          view: Gerrit.Nav.View.DASHBOARD,
          project: 'gerrit/project',
          dashboard: 'default:main',
        };
        assert.equal(
            element._generateUrl(params),
            '/p/gerrit/project/+/dashboard/default:main');
      });
    });

    suite('groups', () => {
      test('group info', () => {
        const params = {
          view: Gerrit.Nav.View.GROUP,
          groupId: 1234,
        };
        assert.equal(element._generateUrl(params), '/admin/groups/1234');
      });

      test('group members', () => {
        const params = {
          view: Gerrit.Nav.View.GROUP,
          groupId: 1234,
          detail: 'members',
        };
        assert.equal(element._generateUrl(params),
            '/admin/groups/1234,members');
      });

      test('group audit log', () => {
        const params = {
          view: Gerrit.Nav.View.GROUP,
          groupId: 1234,
          detail: 'log',
        };
        assert.equal(element._generateUrl(params),
            '/admin/groups/1234,audit-log');
      });
    });
  });

  suite('param normalization', () => {
    let projectLookupStub;

    setup(() => {
      projectLookupStub = sandbox
          .stub(element.$.restAPI, 'getFromProjectLookup');
      sandbox.stub(element, '_generateUrl');
    });

    suite('_normalizeLegacyRouteParams', () => {
      let rangeStub;
      let redirectStub;
      let show404Stub;

      setup(() => {
        rangeStub = sandbox.stub(element, '_normalizePatchRangeParams')
            .returns(Promise.resolve());
        redirectStub = sandbox.stub(element, '_redirect');
        show404Stub = sandbox.stub(element, '_show404');
      });

      test('w/o changeNum', () => {
        projectLookupStub.returns(Promise.resolve('foo/bar'));
        const params = {};
        return element._normalizeLegacyRouteParams(params).then(() => {
          assert.isFalse(projectLookupStub.called);
          assert.isFalse(rangeStub.called);
          assert.isNotOk(params.project);
          assert.isFalse(redirectStub.called);
          assert.isFalse(show404Stub.called);
        });
      });

      test('w/ changeNum', () => {
        projectLookupStub.returns(Promise.resolve('foo/bar'));
        const params = {changeNum: 1234};
        return element._normalizeLegacyRouteParams(params).then(() => {
          assert.isTrue(projectLookupStub.called);
          assert.isTrue(rangeStub.called);
          assert.equal(params.project, 'foo/bar');
          assert.isTrue(redirectStub.calledOnce);
          assert.isFalse(show404Stub.called);
        });
      });

      test('halts on project lookup failure', () => {
        projectLookupStub.returns(Promise.resolve(undefined));
        const params = {changeNum: 1234};
        return element._normalizeLegacyRouteParams(params).then(() => {
          assert.isTrue(projectLookupStub.called);
          assert.isFalse(rangeStub.called);
          assert.isUndefined(params.project);
          assert.isFalse(redirectStub.called);
          assert.isTrue(show404Stub.calledOnce);
        });
      });
    });

    suite('_normalizePatchRangeParams', () => {
      test('range n..n normalizes to n', () => {
        const params = {basePatchNum: 4, patchNum: 4};
        const needsRedirect = element._normalizePatchRangeParams(params);
        assert.isTrue(needsRedirect);
        assert.isNotOk(params.basePatchNum);
        assert.equal(params.patchNum, 4);
      });

      test('range n.. normalizes to n', () => {
        const params = {basePatchNum: 4};
        const needsRedirect = element._normalizePatchRangeParams(params);
        assert.isFalse(needsRedirect);
        assert.isNotOk(params.basePatchNum);
        assert.equal(params.patchNum, 4);
      });
    });
  });

  suite('route handlers', () => {
    let redirectStub;
    let setParamsStub;
    let handlePassThroughRoute;

    // Simple route handlers are direct mappings from parsed route data to a
    // new set of app.params. This test helper asserts that passing `data`
    // into `methodName` results in setting the params specified in `params`.
    function assertDataToParams(data, methodName, params) {
      element[methodName](data);
      assert.deepEqual(setParamsStub.lastCall.args[0], params);
    }

    setup(() => {
      redirectStub = sandbox.stub(element, '_redirect');
      setParamsStub = sandbox.stub(element, '_setParams');
      handlePassThroughRoute = sandbox.stub(element, '_handlePassThroughRoute');
    });

    test('_handleAgreementsRoute', () => {
      const data = {params: {}};
      element._handleAgreementsRoute(data);
      assert.isTrue(redirectStub.calledOnce);
      assert.equal(redirectStub.lastCall.args[0], '/settings/#Agreements');
    });

    test('_handleNewAgreementsRoute', () => {
      element._handleNewAgreementsRoute({params: {}});
      assert.isTrue(setParamsStub.calledOnce);
      assert.equal(setParamsStub.lastCall.args[0].view,
          Gerrit.Nav.View.AGREEMENTS);
    });

    test('_handleSettingsLegacyRoute', () => {
      const data = {params: {0: 'my-token'}};
      assertDataToParams(data, '_handleSettingsLegacyRoute', {
        view: Gerrit.Nav.View.SETTINGS,
        emailToken: 'my-token',
      });
    });

    test('_handleSettingsLegacyRoute with +', () => {
      const data = {params: {0: 'my-token test'}};
      assertDataToParams(data, '_handleSettingsLegacyRoute', {
        view: Gerrit.Nav.View.SETTINGS,
        emailToken: 'my-token+test',
      });
    });

    test('_handleSettingsRoute', () => {
      const data = {};
      assertDataToParams(data, '_handleSettingsRoute', {
        view: Gerrit.Nav.View.SETTINGS,
      });
    });

    test('_handleDefaultRoute on first load', () => {
      const appElementStub = {dispatchEvent: sinon.stub()};
      element._appElement = () => appElementStub;
      element._handleDefaultRoute();
      assert.isTrue(appElementStub.dispatchEvent.calledOnce);
      assert.equal(
          appElementStub.dispatchEvent.lastCall.args[0].detail.response.status,
          404);
    });

    test('_handleDefaultRoute after internal navigation', () => {
      let onExit = null;
      const onRegisteringExit = (match, _onExit) => {
        onExit = _onExit;
      };
      sandbox.stub(window.page, 'exit', onRegisteringExit);
      sandbox.stub(Gerrit.Nav, 'setup');
      sandbox.stub(window.page, 'start');
      sandbox.stub(window.page, 'base');
      sandbox.stub(window, 'page');
      element._startRouter();

      const appElementStub = {dispatchEvent: sinon.stub()};
      element._appElement = () => appElementStub;
      element._handleDefaultRoute();

      onExit('', () => {}); // we left page;

      element._handleDefaultRoute();
      assert.isTrue(handlePassThroughRoute.calledOnce);
    });

    test('_handleImproperlyEncodedPlusRoute', () => {
      // Regression test for Issue 7100.
      element._handleImproperlyEncodedPlusRoute(
          {canonicalPath: '/c/test/%20/42', params: ['test', '42']});
      assert.isTrue(redirectStub.calledOnce);
      assert.equal(
          redirectStub.lastCall.args[0],
          '/c/test/+/42');

      sandbox.stub(element, '_getHashFromCanonicalPath').returns('foo');
      element._handleImproperlyEncodedPlusRoute(
          {canonicalPath: '/c/test/%20/42', params: ['test', '42']});
      assert.equal(
          redirectStub.lastCall.args[0],
          '/c/test/+/42#foo');
    });

    test('_handleQueryRoute', () => {
      const data = {params: ['project:foo/bar/baz']};
      assertDataToParams(data, '_handleQueryRoute', {
        view: Gerrit.Nav.View.SEARCH,
        query: 'project:foo/bar/baz',
        offset: undefined,
      });

      data.params.push(',123', '123');
      assertDataToParams(data, '_handleQueryRoute', {
        view: Gerrit.Nav.View.SEARCH,
        query: 'project:foo/bar/baz',
        offset: '123',
      });
    });

    test('_handleQueryLegacySuffixRoute', () => {
      element._handleQueryLegacySuffixRoute({path: '/q/foo+bar,n,z'});
      assert.isTrue(redirectStub.calledOnce);
      assert.equal(redirectStub.lastCall.args[0], '/q/foo+bar');
    });

    test('_handleQueryRoute', () => {
      const data = {params: ['project:foo/bar/baz']};
      assertDataToParams(data, '_handleQueryRoute', {
        view: Gerrit.Nav.View.SEARCH,
        query: 'project:foo/bar/baz',
        offset: undefined,
      });

      data.params.push(',123', '123');
      assertDataToParams(data, '_handleQueryRoute', {
        view: Gerrit.Nav.View.SEARCH,
        query: 'project:foo/bar/baz',
        offset: '123',
      });
    });

    suite('_handleRegisterRoute', () => {
      test('happy path', () => {
        const ctx = {params: ['/foo/bar']};
        element._handleRegisterRoute(ctx);
        assert.isTrue(redirectStub.calledWithExactly('/foo/bar'));
        assert.isTrue(setParamsStub.calledOnce);
        assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
      });

      test('no param', () => {
        const ctx = {params: ['']};
        element._handleRegisterRoute(ctx);
        assert.isTrue(redirectStub.calledWithExactly('/'));
        assert.isTrue(setParamsStub.calledOnce);
        assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
      });

      test('prevent redirect', () => {
        const ctx = {params: ['/register']};
        element._handleRegisterRoute(ctx);
        assert.isTrue(redirectStub.calledWithExactly('/'));
        assert.isTrue(setParamsStub.calledOnce);
        assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
      });
    });

    suite('_handleRootRoute', () => {
      test('closes for closeAfterLogin', () => {
        const data = {querystring: 'closeAfterLogin', canonicalPath: ''};
        const closeStub = sandbox.stub(window, 'close');
        const result = element._handleRootRoute(data);
        assert.isNotOk(result);
        assert.isTrue(closeStub.called);
        assert.isFalse(redirectStub.called);
      });

      test('redirects to dashboard if logged in', () => {
        sandbox.stub(element.$.restAPI, 'getLoggedIn')
            .returns(Promise.resolve(true));
        const data = {
          canonicalPath: '/', path: '/', querystring: '', hash: '',
        };
        const result = element._handleRootRoute(data);
        assert.isOk(result);
        return result.then(() => {
          assert.isTrue(redirectStub.calledWithExactly('/dashboard/self'));
        });
      });

      test('redirects to open changes if not logged in', () => {
        sandbox.stub(element.$.restAPI, 'getLoggedIn')
            .returns(Promise.resolve(false));
        const data = {
          canonicalPath: '/', path: '/', querystring: '', hash: '',
        };
        const result = element._handleRootRoute(data);
        assert.isOk(result);
        return result.then(() => {
          assert.isTrue(redirectStub.calledWithExactly('/q/status:open'));
        });
      });

      suite('GWT hash-path URLs', () => {
        test('redirects hash-path URLs', () => {
          const data = {
            canonicalPath: '/#/foo/bar/baz',
            hash: '/foo/bar/baz',
            querystring: '',
          };
          const result = element._handleRootRoute(data);
          assert.isNotOk(result);
          assert.isTrue(redirectStub.called);
          assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz'));
        });

        test('redirects hash-path URLs w/o leading slash', () => {
          const data = {
            canonicalPath: '/#foo/bar/baz',
            querystring: '',
            hash: 'foo/bar/baz',
          };
          const result = element._handleRootRoute(data);
          assert.isNotOk(result);
          assert.isTrue(redirectStub.called);
          assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz'));
        });

        test('normalizes "/ /" in hash to "/+/"', () => {
          const data = {
            canonicalPath: '/#/foo/bar/+/123/4',
            querystring: '',
            hash: '/foo/bar/ /123/4',
          };
          const result = element._handleRootRoute(data);
          assert.isNotOk(result);
          assert.isTrue(redirectStub.called);
          assert.isTrue(redirectStub.calledWithExactly('/foo/bar/+/123/4'));
        });

        test('prepends baseurl to hash-path', () => {
          const data = {
            canonicalPath: '/#/foo/bar',
            querystring: '',
            hash: '/foo/bar',
          };
          sandbox.stub(element, 'getBaseUrl').returns('/baz');
          const result = element._handleRootRoute(data);
          assert.isNotOk(result);
          assert.isTrue(redirectStub.called);
          assert.isTrue(redirectStub.calledWithExactly('/baz/foo/bar'));
        });

        test('normalizes /VE/ settings hash-paths', () => {
          const data = {
            canonicalPath: '/#/VE/foo/bar',
            querystring: '',
            hash: '/VE/foo/bar',
          };
          const result = element._handleRootRoute(data);
          assert.isNotOk(result);
          assert.isTrue(redirectStub.called);
          assert.isTrue(redirectStub.calledWithExactly(
              '/settings/VE/foo/bar'));
        });

        test('does not drop "inner hashes"', () => {
          const data = {
            canonicalPath: '/#/foo/bar#baz',
            querystring: '',
            hash: '/foo/bar',
          };
          const result = element._handleRootRoute(data);
          assert.isNotOk(result);
          assert.isTrue(redirectStub.called);
          assert.isTrue(redirectStub.calledWithExactly('/foo/bar#baz'));
        });
      });
    });

    suite('_handleDashboardRoute', () => {
      let redirectToLoginStub;

      setup(() => {
        redirectToLoginStub = sandbox.stub(element, '_redirectToLogin');
      });

      test('own dashboard but signed out redirects to login', () => {
        sandbox.stub(element.$.restAPI, 'getLoggedIn')
            .returns(Promise.resolve(false));
        const data = {canonicalPath: '/dashboard/', params: {0: 'seLF'}};
        return element._handleDashboardRoute(data, '').then(() => {
          assert.isTrue(redirectToLoginStub.calledOnce);
          assert.isFalse(redirectStub.called);
          assert.isFalse(setParamsStub.called);
        });
      });

      test('non-self dashboard but signed out does not redirect', () => {
        sandbox.stub(element.$.restAPI, 'getLoggedIn')
            .returns(Promise.resolve(false));
        const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}};
        return element._handleDashboardRoute(data, '').then(() => {
          assert.isFalse(redirectToLoginStub.called);
          assert.isFalse(setParamsStub.called);
          assert.isTrue(redirectStub.calledOnce);
          assert.equal(redirectStub.lastCall.args[0], '/q/owner:foo');
        });
      });

      test('dashboard while signed in sets params', () => {
        sandbox.stub(element.$.restAPI, 'getLoggedIn')
            .returns(Promise.resolve(true));
        const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}};
        return element._handleDashboardRoute(data, '').then(() => {
          assert.isFalse(redirectToLoginStub.called);
          assert.isFalse(redirectStub.called);
          assert.isTrue(setParamsStub.calledOnce);
          assert.deepEqual(setParamsStub.lastCall.args[0], {
            view: Gerrit.Nav.View.DASHBOARD,
            user: 'foo',
          });
        });
      });
    });

    suite('_handleCustomDashboardRoute', () => {
      let redirectToLoginStub;

      setup(() => {
        redirectToLoginStub = sandbox.stub(element, '_redirectToLogin');
      });

      test('no user specified', () => {
        const data = {canonicalPath: '/dashboard/', params: {0: ''}};
        return element._handleCustomDashboardRoute(data, '').then(() => {
          assert.isFalse(setParamsStub.called);
          assert.isTrue(redirectStub.called);
          assert.equal(redirectStub.lastCall.args[0], '/dashboard/self');
        });
      });

      test('custom dashboard without title', () => {
        const data = {canonicalPath: '/dashboard/', params: {0: ''}};
        return element._handleCustomDashboardRoute(data, '?a=b&c&d=e')
            .then(() => {
              assert.isFalse(redirectStub.called);
              assert.isTrue(setParamsStub.calledOnce);
              assert.deepEqual(setParamsStub.lastCall.args[0], {
                view: Gerrit.Nav.View.DASHBOARD,
                user: 'self',
                sections: [
                  {name: 'a', query: 'b'},
                  {name: 'd', query: 'e'},
                ],
                title: 'Custom Dashboard',
              });
            });
      });

      test('custom dashboard with title', () => {
        const data = {canonicalPath: '/dashboard/', params: {0: ''}};
        return element._handleCustomDashboardRoute(data,
            '?a=b&c&d=&=e&title=t')
            .then(() => {
              assert.isFalse(redirectToLoginStub.called);
              assert.isFalse(redirectStub.called);
              assert.isTrue(setParamsStub.calledOnce);
              assert.deepEqual(setParamsStub.lastCall.args[0], {
                view: Gerrit.Nav.View.DASHBOARD,
                user: 'self',
                sections: [
                  {name: 'a', query: 'b'},
                ],
                title: 't',
              });
            });
      });

      test('custom dashboard with foreach', () => {
        const data = {canonicalPath: '/dashboard/', params: {0: ''}};
        return element._handleCustomDashboardRoute(data,
            '?a=b&c&d=&=e&foreach=is:open')
            .then(() => {
              assert.isFalse(redirectToLoginStub.called);
              assert.isFalse(redirectStub.called);
              assert.isTrue(setParamsStub.calledOnce);
              assert.deepEqual(setParamsStub.lastCall.args[0], {
                view: Gerrit.Nav.View.DASHBOARD,
                user: 'self',
                sections: [
                  {name: 'a', query: 'is:open b'},
                ],
                title: 'Custom Dashboard',
              });
            });
      });
    });

    suite('group routes', () => {
      test('_handleGroupInfoRoute', () => {
        const data = {params: {0: 1234}};
        element._handleGroupInfoRoute(data);
        assert.isTrue(redirectStub.calledOnce);
        assert.equal(redirectStub.lastCall.args[0], '/admin/groups/1234');
      });

      test('_handleGroupAuditLogRoute', () => {
        const data = {params: {0: 1234}};
        assertDataToParams(data, '_handleGroupAuditLogRoute', {
          view: Gerrit.Nav.View.GROUP,
          detail: 'log',
          groupId: 1234,
        });
      });

      test('_handleGroupMembersRoute', () => {
        const data = {params: {0: 1234}};
        assertDataToParams(data, '_handleGroupMembersRoute', {
          view: Gerrit.Nav.View.GROUP,
          detail: 'members',
          groupId: 1234,
        });
      });

      test('_handleGroupListOffsetRoute', () => {
        const data = {params: {}};
        assertDataToParams(data, '_handleGroupListOffsetRoute', {
          view: Gerrit.Nav.View.ADMIN,
          adminView: 'gr-admin-group-list',
          offset: 0,
          filter: null,
          openCreateModal: false,
        });

        data.params[1] = 42;
        assertDataToParams(data, '_handleGroupListOffsetRoute', {
          view: Gerrit.Nav.View.ADMIN,
          adminView: 'gr-admin-group-list',
          offset: 42,
          filter: null,
          openCreateModal: false,
        });

        data.hash = 'create';
        assertDataToParams(data, '_handleGroupListOffsetRoute', {
          view: Gerrit.Nav.View.ADMIN,
          adminView: 'gr-admin-group-list',
          offset: 42,
          filter: null,
          openCreateModal: true,
        });
      });

      test('_handleGroupListFilterOffsetRoute', () => {
        const data = {params: {filter: 'foo', offset: 42}};
        assertDataToParams(data, '_handleGroupListFilterOffsetRoute', {
          view: Gerrit.Nav.View.ADMIN,
          adminView: 'gr-admin-group-list',
          offset: 42,
          filter: 'foo',
        });
      });

      test('_handleGroupListFilterRoute', () => {
        const data = {params: {filter: 'foo'}};
        assertDataToParams(data, '_handleGroupListFilterRoute', {
          view: Gerrit.Nav.View.ADMIN,
          adminView: 'gr-admin-group-list',
          filter: 'foo',
        });
      });

      test('_handleGroupRoute', () => {
        const data = {params: {0: 4321}};
        assertDataToParams(data, '_handleGroupRoute', {
          view: Gerrit.Nav.View.GROUP,
          groupId: 4321,
        });
      });
    });

    suite('repo routes', () => {
      test('_handleProjectsOldRoute', () => {
        const data = {params: {}};
        element._handleProjectsOldRoute(data);
        assert.isTrue(redirectStub.calledOnce);
        assert.equal(redirectStub.lastCall.args[0], '/admin/repos/');
      });

      test('_handleProjectsOldRoute test', () => {
        const data = {params: {1: 'test'}};
        element._handleProjectsOldRoute(data);
        assert.isTrue(redirectStub.calledOnce);
        assert.equal(redirectStub.lastCall.args[0], '/admin/repos/test');
      });

      test('_handleProjectsOldRoute test,branches', () => {
        const data = {params: {1: 'test,branches'}};
        element._handleProjectsOldRoute(data);
        assert.isTrue(redirectStub.calledOnce);
        assert.equal(
            redirectStub.lastCall.args[0], '/admin/repos/test,branches');
      });

      test('_handleRepoRoute', () => {
        const data = {params: {0: 4321}};
        assertDataToParams(data, '_handleRepoRoute', {
          view: Gerrit.Nav.View.REPO,
          repo: 4321,
        });
      });

      test('_handleRepoCommandsRoute', () => {
        const data = {params: {0: 4321}};
        assertDataToParams(data, '_handleRepoCommandsRoute', {
          view: Gerrit.Nav.View.REPO,
          detail: Gerrit.Nav.RepoDetailView.COMMANDS,
          repo: 4321,
        });
      });

      test('_handleRepoAccessRoute', () => {
        const data = {params: {0: 4321}};
        assertDataToParams(data, '_handleRepoAccessRoute', {
          view: Gerrit.Nav.View.REPO,
          detail: Gerrit.Nav.RepoDetailView.ACCESS,
          repo: 4321,
        });
      });

      suite('branch list routes', () => {
        test('_handleBranchListOffsetRoute', () => {
          const data = {params: {0: 4321}};
          assertDataToParams(data, '_handleBranchListOffsetRoute', {
            view: Gerrit.Nav.View.REPO,
            detail: Gerrit.Nav.RepoDetailView.BRANCHES,
            repo: 4321,
            offset: 0,
            filter: null,
          });

          data.params[2] = 42;
          assertDataToParams(data, '_handleBranchListOffsetRoute', {
            view: Gerrit.Nav.View.REPO,
            detail: Gerrit.Nav.RepoDetailView.BRANCHES,
            repo: 4321,
            offset: 42,
            filter: null,
          });
        });

        test('_handleBranchListFilterOffsetRoute', () => {
          const data = {params: {repo: 4321, filter: 'foo', offset: 42}};
          assertDataToParams(data, '_handleBranchListFilterOffsetRoute', {
            view: Gerrit.Nav.View.REPO,
            detail: Gerrit.Nav.RepoDetailView.BRANCHES,
            repo: 4321,
            offset: 42,
            filter: 'foo',
          });
        });

        test('_handleBranchListFilterRoute', () => {
          const data = {params: {repo: 4321, filter: 'foo'}};
          assertDataToParams(data, '_handleBranchListFilterRoute', {
            view: Gerrit.Nav.View.REPO,
            detail: Gerrit.Nav.RepoDetailView.BRANCHES,
            repo: 4321,
            filter: 'foo',
          });
        });
      });

      suite('tag list routes', () => {
        test('_handleTagListOffsetRoute', () => {
          const data = {params: {0: 4321}};
          assertDataToParams(data, '_handleTagListOffsetRoute', {
            view: Gerrit.Nav.View.REPO,
            detail: Gerrit.Nav.RepoDetailView.TAGS,
            repo: 4321,
            offset: 0,
            filter: null,
          });
        });

        test('_handleTagListFilterOffsetRoute', () => {
          const data = {params: {repo: 4321, filter: 'foo', offset: 42}};
          assertDataToParams(data, '_handleTagListFilterOffsetRoute', {
            view: Gerrit.Nav.View.REPO,
            detail: Gerrit.Nav.RepoDetailView.TAGS,
            repo: 4321,
            offset: 42,
            filter: 'foo',
          });
        });

        test('_handleTagListFilterRoute', () => {
          const data = {params: {repo: 4321}};
          assertDataToParams(data, '_handleTagListFilterRoute', {
            view: Gerrit.Nav.View.REPO,
            detail: Gerrit.Nav.RepoDetailView.TAGS,
            repo: 4321,
            filter: null,
          });

          data.params.filter = 'foo';
          assertDataToParams(data, '_handleTagListFilterRoute', {
            view: Gerrit.Nav.View.REPO,
            detail: Gerrit.Nav.RepoDetailView.TAGS,
            repo: 4321,
            filter: 'foo',
          });
        });
      });

      suite('repo list routes', () => {
        test('_handleRepoListOffsetRoute', () => {
          const data = {params: {}};
          assertDataToParams(data, '_handleRepoListOffsetRoute', {
            view: Gerrit.Nav.View.ADMIN,
            adminView: 'gr-repo-list',
            offset: 0,
            filter: null,
            openCreateModal: false,
          });

          data.params[1] = 42;
          assertDataToParams(data, '_handleRepoListOffsetRoute', {
            view: Gerrit.Nav.View.ADMIN,
            adminView: 'gr-repo-list',
            offset: 42,
            filter: null,
            openCreateModal: false,
          });

          data.hash = 'create';
          assertDataToParams(data, '_handleRepoListOffsetRoute', {
            view: Gerrit.Nav.View.ADMIN,
            adminView: 'gr-repo-list',
            offset: 42,
            filter: null,
            openCreateModal: true,
          });
        });

        test('_handleRepoListFilterOffsetRoute', () => {
          const data = {params: {filter: 'foo', offset: 42}};
          assertDataToParams(data, '_handleRepoListFilterOffsetRoute', {
            view: Gerrit.Nav.View.ADMIN,
            adminView: 'gr-repo-list',
            offset: 42,
            filter: 'foo',
          });
        });

        test('_handleRepoListFilterRoute', () => {
          const data = {params: {}};
          assertDataToParams(data, '_handleRepoListFilterRoute', {
            view: Gerrit.Nav.View.ADMIN,
            adminView: 'gr-repo-list',
            filter: null,
          });

          data.params.filter = 'foo';
          assertDataToParams(data, '_handleRepoListFilterRoute', {
            view: Gerrit.Nav.View.ADMIN,
            adminView: 'gr-repo-list',
            filter: 'foo',
          });
        });
      });
    });

    suite('plugin routes', () => {
      test('_handlePluginListOffsetRoute', () => {
        const data = {params: {}};
        assertDataToParams(data, '_handlePluginListOffsetRoute', {
          view: Gerrit.Nav.View.ADMIN,
          adminView: 'gr-plugin-list',
          offset: 0,
          filter: null,
        });

        data.params[1] = 42;
        assertDataToParams(data, '_handlePluginListOffsetRoute', {
          view: Gerrit.Nav.View.ADMIN,
          adminView: 'gr-plugin-list',
          offset: 42,
          filter: null,
        });
      });

      test('_handlePluginListFilterOffsetRoute', () => {
        const data = {params: {filter: 'foo', offset: 42}};
        assertDataToParams(data, '_handlePluginListFilterOffsetRoute', {
          view: Gerrit.Nav.View.ADMIN,
          adminView: 'gr-plugin-list',
          offset: 42,
          filter: 'foo',
        });
      });

      test('_handlePluginListFilterRoute', () => {
        const data = {params: {}};
        assertDataToParams(data, '_handlePluginListFilterRoute', {
          view: Gerrit.Nav.View.ADMIN,
          adminView: 'gr-plugin-list',
          filter: null,
        });

        data.params.filter = 'foo';
        assertDataToParams(data, '_handlePluginListFilterRoute', {
          view: Gerrit.Nav.View.ADMIN,
          adminView: 'gr-plugin-list',
          filter: 'foo',
        });
      });

      test('_handlePluginListRoute', () => {
        const data = {params: {}};
        assertDataToParams(data, '_handlePluginListRoute', {
          view: Gerrit.Nav.View.ADMIN,
          adminView: 'gr-plugin-list',
        });
      });
    });

    suite('change/diff routes', () => {
      test('_handleChangeNumberLegacyRoute', () => {
        const data = {params: {0: 12345}};
        element._handleChangeNumberLegacyRoute(data);
        assert.isTrue(redirectStub.calledOnce);
        assert.isTrue(redirectStub.calledWithExactly('/c/12345'));
      });

      test('_handleChangeLegacyRoute', () => {
        const normalizeRouteStub = sandbox.stub(element,
            '_normalizeLegacyRouteParams');
        const ctx = {
          params: [
            1234, // 0 Change number
            null, // 1 Unused
            null, // 2 Unused
            6, // 3 Base patch number
            null, // 4 Unused
            9, // 5 Patch number
          ],
          querystring: '',
        };
        element._handleChangeLegacyRoute(ctx);
        assert.isTrue(normalizeRouteStub.calledOnce);
        assert.deepEqual(normalizeRouteStub.lastCall.args[0], {
          changeNum: 1234,
          basePatchNum: 6,
          patchNum: 9,
          view: Gerrit.Nav.View.CHANGE,
          querystring: '',
        });
      });

      test('_handleDiffLegacyRoute', () => {
        const normalizeRouteStub = sandbox.stub(element,
            '_normalizeLegacyRouteParams');
        const ctx = {
          params: [
            1234, // 0 Change number
            null, // 1 Unused
            3, // 2 Base patch number
            null, // 3 Unused
            8, // 4 Patch number
            'foo/bar', // 5 Diff path
          ],
          path: '/c/1234/3..8/foo/bar',
          hash: 'b123',
        };
        element._handleDiffLegacyRoute(ctx);
        assert.isFalse(redirectStub.called);
        assert.isTrue(normalizeRouteStub.calledOnce);
        assert.deepEqual(normalizeRouteStub.lastCall.args[0], {
          changeNum: 1234,
          basePatchNum: 3,
          patchNum: 8,
          view: Gerrit.Nav.View.DIFF,
          path: 'foo/bar',
          lineNum: 123,
          leftSide: true,
        });
      });

      test('_handleLegacyLinenum w/ @321', () => {
        const ctx = {path: '/c/1234/3..8/foo/bar@321'};
        element._handleLegacyLinenum(ctx);
        assert.isTrue(redirectStub.calledOnce);
        assert.isTrue(redirectStub.calledWithExactly(
            '/c/1234/3..8/foo/bar#321'));
      });

      test('_handleLegacyLinenum w/ @b123', () => {
        const ctx = {path: '/c/1234/3..8/foo/bar@b123'};
        element._handleLegacyLinenum(ctx);
        assert.isTrue(redirectStub.calledOnce);
        assert.isTrue(redirectStub.calledWithExactly(
            '/c/1234/3..8/foo/bar#b123'));
      });

      suite('_handleChangeRoute', () => {
        let normalizeRangeStub;

        function makeParams(path, hash) {
          return {
            params: [
              'foo/bar', // 0 Project
              1234, // 1 Change number
              null, // 2 Unused
              null, // 3 Unused
              4, // 4 Base patch number
              null, // 5 Unused
              7, // 6 Patch number
            ],
          };
        }

        setup(() => {
          normalizeRangeStub = sandbox.stub(element,
              '_normalizePatchRangeParams');
          sandbox.stub(element.$.restAPI, 'setInProjectLookup');
        });

        test('needs redirect', () => {
          normalizeRangeStub.returns(true);
          sandbox.stub(element, '_generateUrl').returns('foo');
          const ctx = makeParams(null, '');
          element._handleChangeRoute(ctx);
          assert.isTrue(normalizeRangeStub.called);
          assert.isFalse(setParamsStub.called);
          assert.isTrue(redirectStub.calledOnce);
          assert.isTrue(redirectStub.calledWithExactly('foo'));
        });

        test('change view', () => {
          normalizeRangeStub.returns(false);
          sandbox.stub(element, '_generateUrl').returns('foo');
          const ctx = makeParams(null, '');
          assertDataToParams(ctx, '_handleChangeRoute', {
            view: Gerrit.Nav.View.CHANGE,
            project: 'foo/bar',
            changeNum: 1234,
            basePatchNum: 4,
            patchNum: 7,
          });
          assert.isFalse(redirectStub.called);
          assert.isTrue(normalizeRangeStub.called);
        });
      });

      suite('_handleDiffRoute', () => {
        let normalizeRangeStub;

        function makeParams(path, hash) {
          return {
            params: [
              'foo/bar', // 0 Project
              1234, // 1 Change number
              null, // 2 Unused
              null, // 3 Unused
              4, // 4 Base patch number
              null, // 5 Unused
              7, // 6 Patch number
              null, // 7 Unused,
              path, // 8 Diff path
            ],
            hash,
          };
        }

        setup(() => {
          normalizeRangeStub = sandbox.stub(element,
              '_normalizePatchRangeParams');
          sandbox.stub(element.$.restAPI, 'setInProjectLookup');
        });

        test('needs redirect', () => {
          normalizeRangeStub.returns(true);
          sandbox.stub(element, '_generateUrl').returns('foo');
          const ctx = makeParams(null, '');
          element._handleDiffRoute(ctx);
          assert.isTrue(normalizeRangeStub.called);
          assert.isFalse(setParamsStub.called);
          assert.isTrue(redirectStub.calledOnce);
          assert.isTrue(redirectStub.calledWithExactly('foo'));
        });

        test('diff view', () => {
          normalizeRangeStub.returns(false);
          sandbox.stub(element, '_generateUrl').returns('foo');
          const ctx = makeParams('foo/bar/baz', 'b44');
          assertDataToParams(ctx, '_handleDiffRoute', {
            view: Gerrit.Nav.View.DIFF,
            project: 'foo/bar',
            changeNum: 1234,
            basePatchNum: 4,
            patchNum: 7,
            path: 'foo/bar/baz',
            leftSide: true,
            lineNum: 44,
          });
          assert.isFalse(redirectStub.called);
          assert.isTrue(normalizeRangeStub.called);
        });
      });

      test('_handleDiffEditRoute', () => {
        const normalizeRangeSpy =
            sandbox.spy(element, '_normalizePatchRangeParams');
        sandbox.stub(element.$.restAPI, 'setInProjectLookup');
        const ctx = {
          params: [
            'foo/bar', // 0 Project
            1234, // 1 Change number
            3, // 2 Patch num
            'foo/bar/baz', // 3 File path
          ],
        };
        const appParams = {
          project: 'foo/bar',
          changeNum: 1234,
          view: Gerrit.Nav.View.EDIT,
          path: 'foo/bar/baz',
          patchNum: 3,
          lineNum: undefined,
        };

        element._handleDiffEditRoute(ctx);
        assert.isFalse(redirectStub.called);
        assert.isTrue(normalizeRangeSpy.calledOnce);
        assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
        assert.isFalse(normalizeRangeSpy.lastCall.returnValue);
        assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
      });

      test('_handleDiffEditRoute with lineNum', () => {
        const normalizeRangeSpy =
            sandbox.spy(element, '_normalizePatchRangeParams');
        sandbox.stub(element.$.restAPI, 'setInProjectLookup');
        const ctx = {
          params: [
            'foo/bar', // 0 Project
            1234, // 1 Change number
            3, // 2 Patch num
            'foo/bar/baz', // 3 File path
          ],
          hash: 4,
        };
        const appParams = {
          project: 'foo/bar',
          changeNum: 1234,
          view: Gerrit.Nav.View.EDIT,
          path: 'foo/bar/baz',
          patchNum: 3,
          lineNum: 4,
        };

        element._handleDiffEditRoute(ctx);
        assert.isFalse(redirectStub.called);
        assert.isTrue(normalizeRangeSpy.calledOnce);
        assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
        assert.isFalse(normalizeRangeSpy.lastCall.returnValue);
        assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
      });

      test('_handleChangeEditRoute', () => {
        const normalizeRangeSpy =
            sandbox.spy(element, '_normalizePatchRangeParams');
        sandbox.stub(element.$.restAPI, 'setInProjectLookup');
        const ctx = {
          params: [
            'foo/bar', // 0 Project
            1234, // 1 Change number
            null,
            3, // 3 Patch num
          ],
        };
        const appParams = {
          project: 'foo/bar',
          changeNum: 1234,
          view: Gerrit.Nav.View.CHANGE,
          patchNum: 3,
          edit: true,
        };

        element._handleChangeEditRoute(ctx);
        assert.isFalse(redirectStub.called);
        assert.isTrue(normalizeRangeSpy.calledOnce);
        assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
        assert.isFalse(normalizeRangeSpy.lastCall.returnValue);
        assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
      });
    });

    test('_handlePluginScreen', () => {
      const ctx = {params: ['foo', 'bar']};
      assertDataToParams(ctx, '_handlePluginScreen', {
        view: Gerrit.Nav.View.PLUGIN_SCREEN,
        plugin: 'foo',
        screen: 'bar',
      });
      assert.isFalse(redirectStub.called);
    });
  });

  suite('_parseQueryString', () => {
    test('empty queries', () => {
      assert.deepEqual(element._parseQueryString(''), []);
      assert.deepEqual(element._parseQueryString('?'), []);
      assert.deepEqual(element._parseQueryString('??'), []);
      assert.deepEqual(element._parseQueryString('&&&'), []);
    });

    test('url decoding', () => {
      assert.deepEqual(element._parseQueryString('+'), [[' ', '']]);
      assert.deepEqual(element._parseQueryString('???+%3d+'), [[' = ', '']]);
      assert.deepEqual(
          element._parseQueryString('%6e%61%6d%65=%76%61%6c%75%65'),
          [['name', 'value']]);
    });

    test('multiple parameters', () => {
      assert.deepEqual(
          element._parseQueryString('a=b&c=d&e=f'),
          [['a', 'b'], ['c', 'd'], ['e', 'f']]);
      assert.deepEqual(
          element._parseQueryString('&a=b&&&e=f&'),
          [['a', 'b'], ['e', 'f']]);
    });
  });
});
</script>
